/*********************************************************************
 * Copyright (C) 2002 Andrew Khan
 * <p>
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * <p>
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * <p>
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ***************************************************************************/

package jxl.biff;

import jxl.WorkbookSettings;
import jxl.common.Logger;
import jxl.format.Format;
import jxl.read.biff.Record;

import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;

/**
 * A non-built in format record
 */
public class FormatRecord extends WritableRecordData
        implements DisplayFormat, Format {
    /**
     * The logger
     */
    public static Logger logger = Logger.getLogger(FormatRecord.class);

    /**
     * Initialized flag
     */
    private boolean initialized;

    /**
     * The raw data
     */
    private byte[] data;

    /**
     * The index code
     */
    private int indexCode;

    /**
     * The formatting string
     */
    private String formatString;

    /**
     * Indicates whether this is a date formatting record
     */
    private boolean date;

    /**
     * Indicates whether this a number formatting record
     */
    private boolean number;

    /**
     * The format object
     */
    private java.text.Format format;

    /**
     * The date strings to look for
     */
    private static String[] dateStrings = new String[]
            {
                    "dd",
                    "mm",
                    "yy",
                    "hh",
                    "ss",
                    "m/",
                    "/d"
            };

    // Type to distinguish between biff7 and biff8
    private static class BiffType {
    }

    public static final BiffType biff8 = new BiffType();
    public static final BiffType biff7 = new BiffType();

    /**
     * Constructor invoked when copying sheets
     *
     * @param fmt the format string
     * @param refno the index code
     */
    FormatRecord(String fmt, int refno) {
        super(Type.FORMAT);
        formatString = fmt;
        indexCode = refno;
        initialized = true;
    }

    /**
     * Constructor used by writable formats
     */
    protected FormatRecord() {
        super(Type.FORMAT);
        initialized = false;
    }

    /**
     * Copy constructor - can be invoked by public access
     *
     * @param fr the format to copy
     */
    protected FormatRecord(FormatRecord fr) {
        super(Type.FORMAT);
        initialized = false;

        formatString = fr.formatString;
        date = fr.date;
        number = fr.number;
        //    format = (java.text.Format) fr.format.clone();
    }

    /**
     * Constructs this object from the raw data.  Used when reading in a
     * format record
     *
     * @param t the raw data
     * @param ws the workbook settings
     * @param biffType biff type dummy overload
     */
    public FormatRecord(Record t, WorkbookSettings ws, BiffType biffType) {
        super(t);

        byte[] data = getRecord().getData();
        indexCode = IntegerHelper.getInt(data[0], data[1]);
        initialized = true;

        if (biffType == biff8) {
            int numchars = IntegerHelper.getInt(data[2], data[3]);
            if (data[4] == 0) {
                formatString = StringHelper.getString(data, numchars, 5, ws);
            } else {
                formatString = StringHelper.getUnicodeString(data, numchars, 5);
            }
        } else {
            int numchars = data[2];
            byte[] chars = new byte[numchars];
            System.arraycopy(data, 3, chars, 0, chars.length);
            formatString = new String(chars);
        }

        date = false;
        number = false;

        // First see if this is a date format
        for (int i = 0; i < dateStrings.length; i++) {
            String dateString = dateStrings[i];
            if (formatString.indexOf(dateString) != -1 ||
                    formatString.indexOf(dateString.toUpperCase()) != -1) {
                date = true;
                break;
            }
        }

        // See if this is number format - look for the # or 0 characters
        if (!date) {
            if (formatString.indexOf('#') != -1 ||
                    formatString.indexOf('0') != -1) {
                number = true;
            }
        }
    }

    /**
     * Used to get the data when writing out the format record
     *
     * @return the raw data
     */
    public byte[] getData() {
        data = new byte[formatString.length() * 2 + 3 + 2];

        IntegerHelper.getTwoBytes(indexCode, data, 0);
        IntegerHelper.getTwoBytes(formatString.length(), data, 2);
        data[4] = (byte) 1; // unicode indicator
        StringHelper.getUnicodeBytes(formatString, data, 5);

        return data;
    }

    /**
     * Gets the format index of this record
     *
     * @return the format index of this record
     */
    public int getFormatIndex() {
        return indexCode;
    }

    /**
     * Accessor to see whether this object is initialized or not.
     *
     * @return TRUE if this font record has been initialized, FALSE otherwise
     */
    public boolean isInitialized() {
        return initialized;
    }

    /**
     * Sets the index of this record.  Called from the FormattingRecords
     * object
     *
     * @param pos the position of this font in the workbooks font list
     */

    public void initialize(int pos) {
        indexCode = pos;
        initialized = true;
    }

    /**
     * Replaces all instances of search with replace in the input.  Used for
     * replacing microsoft number formatting characters with java equivalents
     *
     * @param input the format string
     * @param search the Excel character to be replaced
     * @param replace the java equivalent
     * @return the input string with the specified substring replaced
     */
    protected final String replace(String input, String search, String replace) {
        String fmtstr = input;
        int pos = fmtstr.indexOf(search);
        while (pos != -1) {
            StringBuffer tmp = new StringBuffer(fmtstr.substring(0, pos));
            tmp.append(replace);
            tmp.append(fmtstr.substring(pos + search.length()));
            fmtstr = tmp.toString();
            pos = fmtstr.indexOf(search);
        }
        return fmtstr;
    }

    /**
     * Called by the immediate subclass to set the string
     * once the Java-Excel replacements have been done
     *
     * @param s the format string
     */
    protected final void setFormatString(String s) {
        formatString = s;
    }

    /**
     * Sees if this format is a date format
     *
     * @return TRUE if this format is a date
     */
    public final boolean isDate() {
        return date;
    }

    /**
     * Sees if this format is a number format
     *
     * @return TRUE if this format is a number
     */
    public final boolean isNumber() {
        return number;
    }

    /**
     * Gets the java equivalent number format for the formatString
     *
     * @return The java equivalent of the number format for this object
     */
    public final NumberFormat getNumberFormat() {
        if (format != null && format instanceof NumberFormat) {
            return (NumberFormat) format;
        }

        try {
            String fs = formatString;

            // Replace the Excel formatting characters with java equivalents
            fs = replace(fs, "E+", "E");
            fs = replace(fs, "_)", "");
            fs = replace(fs, "_", "");
            fs = replace(fs, "[Red]", "");
            fs = replace(fs, "\\", "");

            format = new DecimalFormat(fs);
        } catch (IllegalArgumentException e) {
            // Something went wrong with the date format - fail silently
            // and return a default value
            format = new DecimalFormat("#.###");
        }

        return (NumberFormat) format;
    }

    /**
     * Gets the java equivalent date format for the formatString
     *
     * @return The java equivalent of the date format for this object
     */
    public final DateFormat getDateFormat() {
        if (format != null && format instanceof DateFormat) {
            return (DateFormat) format;
        }

        String fmt = formatString;

        // Replace the AM/PM indicator with an a
        int pos = fmt.indexOf("AM/PM");
        while (pos != -1) {
            StringBuffer sb = new StringBuffer(fmt.substring(0, pos));
            sb.append('a');
            sb.append(fmt.substring(pos + 5));
            fmt = sb.toString();
            pos = fmt.indexOf("AM/PM");
        }

        // Replace ss.0 with ss.SSS (necessary to always specify milliseconds
        // because of NT)
        pos = fmt.indexOf("ss.0");
        while (pos != -1) {
            StringBuffer sb = new StringBuffer(fmt.substring(0, pos));
            sb.append("ss.SSS");

            // Keep going until we run out of zeros
            pos += 4;
            while (pos < fmt.length() && fmt.charAt(pos) == '0') {
                pos++;
            }

            sb.append(fmt.substring(pos));
            fmt = sb.toString();
            pos = fmt.indexOf("ss.0");
        }


        // Filter out the backslashes
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < fmt.length(); i++) {
            if (fmt.charAt(i) != '\\') {
                sb.append(fmt.charAt(i));
            }
        }

        fmt = sb.toString();

        // If the date format starts with anything inside square brackets then
        // filter tham out
        if (fmt.charAt(0) == '[') {
            int end = fmt.indexOf(']');
            if (end != -1) {
                fmt = fmt.substring(end + 1);
            }
        }

        // Get rid of some spurious characters that can creep in
        fmt = replace(fmt, ";@", "");

        // We need to convert the month indicator m, to upper case when we
        // are dealing with dates
        char[] formatBytes = fmt.toCharArray();

        for (int i = 0; i < formatBytes.length; i++) {
            if (formatBytes[i] == 'm') {
                // Firstly, see if the preceding character is also an m.  If so,
                // copy that
                if (i > 0 && (formatBytes[i - 1] == 'm' || formatBytes[i - 1] == 'M')) {
                    formatBytes[i] = formatBytes[i - 1];
                } else {
                    // There is no easy way out.  We have to deduce whether this an
                    // minute or a month?  See which is closest out of the
                    // letters H d s or y
                    // First, h
                    int minuteDist = Integer.MAX_VALUE;
                    for (int j = i - 1; j > 0; j--) {
                        if (formatBytes[j] == 'h') {
                            minuteDist = i - j;
                            break;
                        }
                    }

                    for (int j = i + 1; j < formatBytes.length; j++) {
                        if (formatBytes[j] == 'h') {
                            minuteDist = Math.min(minuteDist, j - i);
                            break;
                        }
                    }

                    for (int j = i - 1; j > 0; j--) {
                        if (formatBytes[j] == 'H') {
                            minuteDist = i - j;
                            break;
                        }
                    }

                    for (int j = i + 1; j < formatBytes.length; j++) {
                        if (formatBytes[j] == 'H') {
                            minuteDist = Math.min(minuteDist, j - i);
                            break;
                        }
                    }

                    // Now repeat for s
                    for (int j = i - 1; j > 0; j--) {
                        if (formatBytes[j] == 's') {
                            minuteDist = Math.min(minuteDist, i - j);
                            break;
                        }
                    }
                    for (int j = i + 1; j < formatBytes.length; j++) {
                        if (formatBytes[j] == 's') {
                            minuteDist = Math.min(minuteDist, j - i);
                            break;
                        }
                    }
                    // We now have the distance of the closest character which could
                    // indicate the the m refers to a minute
                    // Repeat for d and y
                    int monthDist = Integer.MAX_VALUE;
                    for (int j = i - 1; j > 0; j--) {
                        if (formatBytes[j] == 'd') {
                            monthDist = i - j;
                            break;
                        }
                    }

                    for (int j = i + 1; j < formatBytes.length; j++) {
                        if (formatBytes[j] == 'd') {
                            monthDist = Math.min(monthDist, j - i);
                            break;
                        }
                    }
                    // Now repeat for y
                    for (int j = i - 1; j > 0; j--) {
                        if (formatBytes[j] == 'y') {
                            monthDist = Math.min(monthDist, i - j);
                            break;
                        }
                    }
                    for (int j = i + 1; j < formatBytes.length; j++) {
                        if (formatBytes[j] == 'y') {
                            monthDist = Math.min(monthDist, j - i);
                            break;
                        }
                    }

                    if (monthDist < minuteDist) {
                        // The month indicator is closer, so convert to a capital M
                        formatBytes[i] = Character.toUpperCase(formatBytes[i]);
                    } else if ((monthDist == minuteDist) &&
                            (monthDist != Integer.MAX_VALUE)) {
                        // They are equidistant.  As a tie-breaker, take the formatting
                        // character which precedes the m
                        char ind = formatBytes[i - monthDist];
                        if (ind == 'y' || ind == 'd') {
                            // The preceding item indicates a month measure, so convert
                            formatBytes[i] = Character.toUpperCase(formatBytes[i]);
                        }
                    }
                }
            }
        }

        try {
            this.format = new SimpleDateFormat(new String(formatBytes));
        } catch (IllegalArgumentException e) {
            // There was a spurious character - fail silently
            this.format = new SimpleDateFormat("dd MM yyyy hh:mm:ss");
        }
        return (DateFormat) this.format;
    }

    /**
     * Gets the index code, for use as a hash value
     *
     * @return the ifmt code for this cell
     */
    public int getIndexCode() {
        return indexCode;
    }

    /**
     * Gets the formatting string.
     *
     * @return the excel format string
     */
    public String getFormatString() {
        return formatString;
    }

    /**
     * Indicates whether this formula is a built in
     *
     * @return FALSE
     */
    public boolean isBuiltIn() {
        return false;
    }

    /**
     * Standard hash code method
     * @return the hash code value for this object
     */
    public int hashCode() {
        return formatString.hashCode();
    }

    /**
     * Standard equals method.  This compares the contents of two
     * format records, and not their indexCodes, which are ignored
     *
     * @param o the object to compare
     * @return TRUE if the two objects are equal, FALSE otherwise
     */
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof FormatRecord)) {
            return false;
        }

        FormatRecord fr = (FormatRecord) o;

        // Initialized format comparison
        if (initialized && fr.initialized) {
            // Must be either a number or a date format
            if (date != fr.date ||
                    number != fr.number) {
                return false;
            }

            return formatString.equals(fr.formatString);
        }

        // Uninitialized format comparison
        return formatString.equals(fr.formatString);
    }
}






